// // Copyright (c) 2009 All Right Reserved // // vl // // 2009-01-01 // Contains ... using System; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; using System.Security; using System.Text; using JetBrains.Annotations; namespace LargoCommon.Midi { /// /// Midi Internal. /// internal static class MidiInternalMessages { /// /// Synchronization Lock. /// private static readonly object ThisLock = new object(); /// /// Gets a value indicating whether this instance is open. /// /// /// True if this instance is open; otherwise, false. /// private static bool IsOpen => !string.IsNullOrEmpty(PlayingAlias); /// /// Gets or sets the playing alias. /// /// /// The playing alias. /// private static string PlayingAlias { get; set; } #region Midi Output Messages - sending /// /// Send Midi Message. /// /// Midi Command Code. /// Midi Channel. /// Data Value1. /// Data Value2. public static void SendMidiMessage(MidiCommandCode givenType, int channel, int dataValue1, int dataValue2 = 0) { lock (ThisLock) { if (MidiInternalDevices.MidiDeviceHandle == null) { MidiInternalDevices.InternalOpenMidi(); } if (MidiInternalDevices.MidiDeviceHandle != null) { SendMidiMessage(MidiInternalDevices.MidiDeviceHandle, ((int)givenType | channel) | (dataValue1 << 8) | (dataValue2 << 16)); } } } #endregion #region SendString /// /// Prepares the midi. /// public static void PrepareMidi() { //// We can't play using MCI if we already have an open handle to the default //// MIDI device. As such, we'll temporarily close it if its open and then //// when we're done reopen it if it was open. if (IsOpen) { MidiFileClose(); } } /// /// Midi FileOpen. /// /// Midi file Path. /// Midi file Alias. public static void MidiFileOpen(string path, string alias) { CheckPath(path); PlayingAlias = alias; //// MidiInternalDevices.PrepareMidi(); //// Open the file, play it, close it MciSendString(string.Format(CultureInfo.InvariantCulture, "open \"{0}\" type mpegvideo alias {1}", path, alias)); } /// /// Midi FilePlay. /// public static void MidiFilePlay() { lock (ThisLock) { //// MidiInternalDevices.MciSendString("play " + alias + " wait"); if (IsOpen) { MciSendString("play " + PlayingAlias); } } } /// /// Midi FileClose. /// public static void MidiFileClose() { if (!IsOpen) { return; } MciSendString("close " + PlayingAlias); PlayingAlias = null; //// Reopen the MIDI device if it was previously open //// if (MidiInternalDevices.IsOpen) { MidiInternalDevices.InternalOpenMidi(); } } /// Plays the specified MIDI file using Media Control Interface (MCI). /// The MIDI file to be played. [UsedImplicitly] public static void PlayFile(string path) { CheckPath(path); //// Play the file using interop calls: open the file, play it (wait for it to finish), close it var alias = Guid.NewGuid().ToString("N", CultureInfo.CurrentCulture); //// randomly generated alias to avoid collisions lock (ThisLock) { //// We can't play using MCI if we already have an open handle to the default //// MIDI device. As such, we'll temporarily close it if its open and then //// when we're done reopen it if it was open. if (MidiInternalDevices.IsOpen) { MidiInternalDevices.InternalCloseMidi(); } //// Open the file, play it, close it MciSendString(string.Format(CultureInfo.InvariantCulture, "open \"{0}\" type mpegvideo alias {1}", path, alias)); MciSendString(string.Format(CultureInfo.InvariantCulture, "play {0} wait", alias)); MciSendString("close " + alias); //// Reopen the MIDI device if it was previously open //// if (MidiInternalDevices.IsOpen) { MidiInternalDevices.InternalOpenMidi(); } } } #endregion #region MCI Commands /// Sends an MCI command. /// The command to be sent. public static void MciSendString(string command) { // Make sure we got a command if (command == null) { throw new ArgumentNullException(nameof(command)); } // Send the command. var rv = NativeMethods.MciSendString(command, null, 0, IntPtr.Zero); if (rv != 0) { MidiInternalDevices.ThrowMciError(rv, string.Format(CultureInfo.InvariantCulture, "Could not execute command '{0}'.", command)); } } #endregion #region /// /// Check Path. /// /// Midi File Path. private static void CheckPath(string path) { // Validate the parameter; make sure the file actually exists if (path == null) { throw new ArgumentNullException(nameof(path)); } if (!File.Exists(path)) { throw new FileNotFoundException("The MIDI file was not found.", path); } } #endregion /// Sends the message to as a short MIDI message to the MIDI output device. /// Handle to the MIDI output device. /// The message to be sent. [HandleProcessCorruptedStateExceptions] [SecurityCritical] private static void SendMidiMessage(MidiDeviceHandle handle, int message) { Contract.Requires(handle != null); lock (ThisLock) { if (handle == null) { MidiInternalDevices.InternalOpenMidi(); } //// if (handle == null) { //// throw new ArgumentNullException("handle", "The handle does not exist. Make sure the MIDI device has been opened."); } //// if (handle == null) { return; } //// 2013/02 if (handle != null) { var h = handle.Handle; if (h == 0) { //// !handle.IsOpen return; } //// An unhandled exception of type 'System.AccessViolationException' occurred in LargoBase.dll //// int result = 0; //// try { //// Attempted to read or write protected memory. This is often an indication that other memory is corrupt. //// When handle.Handle > 2 mil !?! //// result = NativeMethods.MidiOutShortMessage(h, message); //// MidiError result } //// } //// catch (AccessViolationException) { return; } //// 2013/02 The handler is used by another process, such as a callback. //// if (result != 0) { //// MidiError.MidiSystemErrorNOERROR //// MidiInternalDevices.ThrowMciError(result, string.Format(CultureInfo.InvariantCulture, "Could not execute message '{0}'.", message)); //// throw new Exception("Could not send MIDI message: " + Result.ToString(CultureInfo.CurrentCulture)); } } } /// /// Native Methods. /// private static class NativeMethods { #region Native Methods - Low-Level MIDI API - Messages /// The function sends a short MIDI message to the specified MIDI output device. /// Handle to the MIDI output device. /// MIDI message. /// Returns SystemNoError if successful or an error otherwise. [DllImport("winmm.dll", EntryPoint = "midiOutShortMsg", CharSet = CharSet.Ansi)] //// CharSet.Ansi [SuppressMessage("Microsoft.Portability", "CA1901:PInvokeDeclarationsShouldBePortable", MessageId = "return", Justification = "No other result found.")] public static extern int MidiOutShortMessage(int hMidiOut, int dwMsg); //// [DllImport("winmm.dll")] //// protected static extern int midiOutShortMsg(int handle, int message); //// Commented out as Unused //// [DllImport("winmm.dll")] //// protected static extern int midiOutPrepareHeader(int handle, IntPtr headerPtr, int sizeOfMidiHeader); //// Commented out as Unused //// [DllImport("winmm.dll")] //// protected static extern int midiOutUnprepareHeader(int handle, IntPtr headerPtr, int sizeOfMidiHeader); //// Commented out as Unused //// [DllImport("winmm.dll")] //// protected static extern int midiOutLongMsg(int handle, IntPtr headerPtr, int sizeOfMidiHeader); #endregion #region DllImports for MCI /// /// The mciSendString function sends a command string to an MCI device. The device that the /// command is sent to is specified in the command string. /// /// Pointer to a null-terminated string that specifies an MCI command string. /// Pointer to a buffer that receives return information. If no return information is needed, this parameter can be null. /// Size, in characters, of the return buffer specified by the previous parameter. /// Handle to a callback window if the "notify" flag was specified in the command string. /// /// Returns zero if successful or an error otherwise. The low-order word of the returned /// DWORD value contains the error return value. If the error is device-specific, the /// high-order word of the return value is the driver identifier; otherwise, the high-order /// word is zero. /// //// Use string (or System.String) for a const char*, but StringBuilder for a char*. [DllImport("winmm.dll", EntryPoint = "mciSendStringA", CharSet = CharSet.Ansi)] //// CharSet.Ansi //// 11/2010 - MarshalAs public static extern int MciSendString(string lpszCommand, StringBuilder lpszReturn, int cchReturn, IntPtr callbackHandle); //// [DllImport("winmm.dll", EntryPoint = "mciSendStringA", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)] //// public static extern int MciSendString([MarshalAs(UnmanagedType.LPWStr)]string lpszCommand, [MarshalAs(UnmanagedType.LPWStr)]StringBuilder lpszReturnString, int cchReturn, IntPtr callbackHandle); //// public static extern int MciSendString([MarshalAs(UnmanagedType.LPWStr)]string lpszCommand, [MarshalAs(UnmanagedType.LPWStr)]StringBuilder lpszReturnString, int cchReturn, IntPtr callbackHandle); //// [DllImport("winmm.dll", CharSet = CharSet.Ansi, BestFitMapping = true, ThrowOnUnmappableChar = true)] //// [return: MarshalAs(UnmanagedType.U4)] //// public static extern uint mciSendCommand(uint mciId, uint uMsg, uint dwParam1, IntPtr dwParam2); //// //// Sends command string. //// //// Command string. //// Return string. //// Return Length. //// Callback Handle. //// Returns value. //// [DllImport("winmm.dll")] //// Use string (or System.String) for a const char*, but StringBuilder for a char*. //// private static extern long mciSendString([MarshalAs(UnmanagedType.LPWStr)]string commandString, [MarshalAs(UnmanagedType.LPWStr)]StringBuilder returnString, int iReturnLength, IntPtr callbackHandle); //// public static extern long mciSendString(string commandString, StringBuilder returnString, int iReturnLength, IntPtr callbackHandle); #endregion } } }